/*
 * Galaxium Messenger
 * 
 * Copyright (C) 2008 Philippe Durand <draekz@gmail.com>
 * 
 * License: GNU General Public License (GPL)
 * 
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Collections.Generic;

using Galaxium.Core;
using Galaxium.Protocol;

using Anculus.Core;

namespace Galaxium.Protocol.GaduGadu
{
    public delegate void MessageReceivedHandler(ReceiveMessageCommand msg);
    public delegate void MessageAckHandler(SendMessageAckCommand ack);
    public delegate void UserListHandler (UserListEntry entry);
    public delegate void GetInitialStatusHandler (GetInitialStatusCommand cmd);
    public delegate void GetStatusHandler (GetStatusCommand cmd);
    
	public class GaduConnection : IDisposable
	{
		public event EventHandler<ConnectionEventArgs> Established;
        public event MessageReceivedHandler MessageReceived;
        public event MessageAckHandler MessageAck;
        public event UserListHandler UserListEntry;
        public event GetStatusHandler UserStatusUpdate;
        public event GetInitialStatusHandler UserInitialStatusUpdate;
        public event EventHandler UserListComplete;
        public event EventHandler LoginOk;
        public event EventHandler LoginFailed;
        public event EventHandler Ping;
        public event EventHandler Pong;
        public event EventHandler NeedEmail;
        
        protected TCPConnection _connection;
		private List<IGaduCommand> _out_queue = new List<IGaduCommand> ();
		private List<byte> _partial_bytes = new List<byte> ();
		private CommandHeader _partial_header = null;
    //  private int _timeout;
	//	private int _sleep_time;
	//	private Thread _read_thread;
	//	private Thread _write_thread;
	//	private Thread _ping_thread;
	//	private bool _sending = false;
	//	private bool _reconnecting = false;
       	private uint _seed;
		private bool _initial_data = true;
		private uint _ping_timer;
	//	private object _lock;

		public GaduSession Session
		{
			get { return _connection.Session as GaduSession; }
		}
		
		public TCPConnection Connection
		{
			get { return _connection; }
		}
		
		public uint Seed
		{
			get { return _seed; }
		}
		
		public bool IsSending
		{
			get { return _connection.IsSending; }
		}
		
		public bool IsConnected
		{
			get { return _connection.IsConnected; }
		}
		
		public List<IGaduCommand> OutQueue
		{
			get { return _out_queue; }
		}
		
		public GaduConnection (GaduSession session, GaduConnectionInfo info)
		{
			_connection = new TCPConnection (session, info);
			
			_connection.AfterConnect += ConnectionAfterConnect;
			_connection.Closed += ConnectionClosed;
			_connection.ErrorOccurred += ConnectionErrorOccured;
			_connection.DataReceived += ConnectionDataReceived;
			_connection.Established += ConnectionEstablished;
			_connection.SendComplete += ConnectionSendComplete;
			
			_out_queue = new List<IGaduCommand> ();
		}
		
		public void Connect ()
		{
			_connection.Connect();
		}
		
		public void Disconnect ()
		{
			_connection.Disconnect ();
		}
		
		public void Reconnect ()
		{
		//	_reconnecting = true;
			
			Connect ();
		}
		
		public virtual void Dispose ()
		{
			
		}
		
		private void ConnectionAfterConnect (object sender, ConnectionEventArgs args)
		{
			Log.Debug ("GADU: Connection after connect.");
		}
		
		private void ConnectionEstablished (object sender, ConnectionEventArgs args)
		{
			Log.Debug ("GADU: Connection established.");
		}
		
		private void ConnectionClosed (object sender, ConnectionEventArgs args)
		{
			Log.Debug ("GADU: Connection closed.");
			
			if (_ping_timer != 0)
			{
				TimerUtility.RemoveCallback (_ping_timer);
				_ping_timer = 0;
			}
		}
		
		private void ConnectionErrorOccured (object sender, ConnectionErrorEventArgs args)
		{
			Log.Debug ("GADU: Error occured: " + args.ErrorIdentifier + ": "+args.Description);
			
			if (_ping_timer != 0)
			{
				TimerUtility.RemoveCallback (_ping_timer);
				_ping_timer = 0;
			}
		}
		
		private void ConnectionDataReceived (object sender, ConnectionDataEventArgs args)
		{
			MemoryStream stream = new MemoryStream (args.Buffer);
			
			// If this is the first data we are receiving, grab the welcome message.
			
			if (_initial_data)
			{
				Log.Debug ("GADU: Initial command received.");
				
				if (args.Length != 12)
				{
					Log.Error ("GADU: Welcome header data not available! Bytes received: "+args.Length);
					
					return;
				}
				
				WelcomeCommand command = new WelcomeCommand ();
				command.FromStream (stream);
				
				_seed = command.Seed;
				
				Log.Debug ("GADU: Seed obtained: "+_seed);
				
				_initial_data = false;
				
				if (Established != null)
					Established (this, args);
				
				if (_ping_timer == 0)
					_ping_timer = TimerUtility.RequestInfiniteCallback (PingTimerElapsed, 3 * 60 * 1000);
				else
					Log.Warn ("GADU: Ping timer already exists?");
				
				return;
			}
			
			Log.Debug ("GADU: Incoming data size: "+args.Length);
			
			CommandHeader header = new CommandHeader ();
			
			if (_partial_header == null)
			{
            	header.FromStream (stream);
            	Log.Debug ("GADU: Incoming header < Type: "+header.Type+", Content Length: "+header.Length);
				
				if (args.Length - 8 < header.Length)
				{
					Log.Debug ("Starting command from partial data.");
					
					_partial_header = header;
					_partial_bytes.Clear ();
				}
			}
			
			if (_partial_header != null)
			{
				int modifier = 0;
				
				if (_partial_header == header) // Check if its the same object.
					modifier = 8;
				
				for (int i = 0; i < args.Length - modifier; i++)
					_partial_bytes.Add ((byte)stream.ReadByte());
				
				if (_partial_bytes.Count == _partial_header.Length)
				{
					Log.Debug ("Completed command from partial data.");
					
					// Now we have to create a new memory stream with these bytes to read.
					stream = new MemoryStream (_partial_bytes.ToArray());
					
					// Swap the headers for the processing below.
					header = _partial_header;
					
					_partial_header = null;
					_partial_bytes.Clear ();
				}
				else
				{
					Log.Debug ("Not finished yet, more data needed.");
					return;
				}
			}
			
            switch (header.Type)
            {
                case GaduConstants.CMD_IN_RECV_MSG:
                	Log.Debug ("GADU: Received a message from the server.");
                	
					ReceiveMessageCommand msg = new ReceiveMessageCommand ();
                    msg.FromStream (stream);
                    msg.Header = header;
                    
                    if (MessageReceived != null)
                    	MessageReceived (msg);
                    break;
                    
                case GaduConstants.CMD_IN_SEND_MSG_ACK:
                	Log.Debug ("GADU: Received a message ack from the server.");
                	
                    SendMessageAckCommand ack = new SendMessageAckCommand ();
                    ack.FromStream (stream);
                    ack.Header = header;
                    
                    if (MessageAck != null)
                    	MessageAck (ack);
                    break;
                    
                case GaduConstants.CMD_IN_PONG:
                	Log.Debug ("GADU: Pong!");
                	
                	if (Pong != null)
                		Pong (this, new EventArgs());
                    break;
                    
                case GaduConstants.CMD_IN_LOGIN_OK:
                	Log.Debug ("GADU: Received a login ok from the server.");
                	
                    if (header.Length == 1)
                        stream.ReadByte();
                    
                    if (LoginOk != null)
                    	LoginOk (this, new EventArgs());
                    break;
                    
                case GaduConstants.CMD_IN_NEED_EMAIL:
                	Log.Debug ("GADU: Received a need email from the server.");
                	
                	if (NeedEmail != null)
                		NeedEmail (this, new EventArgs ());
                    break;
                    
                case GaduConstants.CMD_IN_LOGIN_FAILED:
                	Log.Debug ("GADU: Received a login failed from the server.");
                	
                    if (LoginFailed != null)
                    	LoginFailed (this, new EventArgs());
                    break;
                
                //case GaduConstants.CMD_IN_STATUS:
					//Log.Debug ("GADU: Received a status from the server.");
                	//break;
                
                //case GaduConstants.CMD_IN_STATUS60:
					//Log.Debug ("GADU: Received a status60 from the server.");
                	//break;
               	
				case GaduConstants.CMD_IN_DISCONNECTING:
					Log.Debug ("GADU: Received a disconnecting from the server.");
					Disconnect ();
					break;
				
				case GaduConstants.CMD_IN_NOTIFY_REPLY:
					Log.Debug ("GADU: Received a notify reply from the server.");
					break;
				
				case GaduConstants.CMD_IN_PUBDIR50_REPLY:
					Log.Debug ("GADU: Received a pubdir50 from the server.");
					break;
				
				case GaduConstants.CMD_IN_USERLIST_REPLY:
					Log.Debug ("GADU: Received a userlist from the server.");
					
					UserListReplyCommand rep = new UserListReplyCommand ();
					rep.FromStream (stream);
					
					switch (rep.Type)
					{
						case GaduConstants.USERLIST_GET_REPLY:
							if (rep.Entries.Count > 0)
							{
								// We have user entries.
								foreach (UserListEntry entry in rep.Entries)
								{
									if (UserListEntry != null)
										UserListEntry (entry);
								}
							}
							else
							{
								Log.Debug ("Retrieved no user entries.");
							}
							
							Log.Debug ("Calling user list complete.");
							
							if (UserListComplete != null)
								UserListComplete (this, new EventArgs ());
							break;
						default:
							Log.Debug ("GADU: Userlist value: "+rep.Type);
							break;
					}
					break;
				
				case GaduConstants.CMD_IN_NOTIFY_REPLY60:
					Log.Debug ("GADU: Received a notify reply60 from the server.");
					break;
				
				case GaduConstants.CMD_IN_STATUS:
					Log.Debug ("GADU: Received a get status from the server.");
					GetStatusCommand getstat = new GetStatusCommand ();
					getstat.FromStream (stream);
					
					if (UserStatusUpdate != null)
						UserStatusUpdate (getstat);
					break;
				
				case GaduConstants.CMD_IN_STATUS_INITIAL:
					Log.Debug ("GADU: Received a get initial status from the server.");
					GetInitialStatusCommand getinit = new GetInitialStatusCommand ();
					getinit.FromStream (stream);
					
					if (UserInitialStatusUpdate != null)
						UserInitialStatusUpdate (getinit);
					break;
				
                default:
                	Log.Warn ("GADU: Unknown data type: "+header.Type);
                	// Make sure we attempt to pass the data
                    for (int i = 0; i < header.Length; i++)
                        stream.ReadByte();
                    break;
            }
            
            ProcessOutQueue ();
		}
		
		private void ConnectionSendComplete (object sender, EventArgs args)
		{
			ProcessOutQueue ();
		}
		
		public void Send (IGaduCommand command, bool force)
		{
			if (force)
			{
				lock (_out_queue)
				{
					_out_queue.Insert (0, command);
				}
			}
			else
			{
				lock (_out_queue)
				{
					_out_queue.Add (command);
				}
				
				ProcessOutQueue ();
			}
		}
		
		protected void ProcessOutQueue ()
		{
			IGaduCommand cmd = null;
			
			lock (_out_queue)
			{
				if (_out_queue.Count == 0)
				{
					//if (Ready != null)
						//Ready (this, EventArgs.Empty);
					return;
				}
				
				cmd = _out_queue[0];
				_out_queue.RemoveAt (0);
			}
			
			if (cmd != null)
				TrueSend (cmd);
			else
				Log.Error ("GADU: Unable to send command, null found!");
		}
		
		private void TrueSend (IGaduCommand cmd)
		{
			MemoryStream stream = new MemoryStream ();
			BinaryWriter writer = new BinaryWriter (stream);
			
			byte[] command_bytes = cmd.ToByteArray();
			cmd.Header.Length = command_bytes.Length;
			
			writer.Write (cmd.Header.Type);
			writer.Write (cmd.Header.Length);
			writer.Write (command_bytes);
			
			stream.Position = 0;
			
			BinaryReader reader = new BinaryReader (stream);
			byte[] bytes = reader.ReadBytes ((int)stream.Length);
			
			Log.Debug ("GADU: Outgoing header > Size: "+stream.Length+", Type: "+cmd.Header.Type+", Content Length: "+cmd.Header.Length);
			
			_connection.Send (bytes);
		}
		
		private void PingTimerElapsed ()
		{
			if (_connection.WasConnected)
            {
            	PingCommand ping = new PingCommand ();
            	
            	Log.Debug ("GADU: Ping!");
            	
            	Send (ping, true);
            	
            	if (Ping != null)
            		Ping (this, new EventArgs ());
            }
		}
	}
}